/*
 * Copyright (c) 2009 LastFM.Desktop Anton Pavlik.
 * All Rights Reserved.
 * 
 * You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, 
 * with or without modifications, and in Source or Object form, provided that You meet the following 
 * conditions:
 * 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
 *
 * 2. You must cause any modified files to carry prominent notices stating that You changed the files; 
 * and
 *
 * 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, 
 * patent, trademark, and attribution notices from the Source form of the Work, excluding those notices 
 * that do not pertain to any part of the Derivative Works.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * You may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, Licensor provides the Work 
 * (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
 * CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties 
 * or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 
 * YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING OR REDISTRIBUTING THE WORK
 * AND ASSUME ANY RISKS ASSOCIATED WITH YOUR EXERCISE OF PERMISSIONS UNDER THIS LICENSE. 
 */
package org.shake.lastfm.data;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class MP3Info
{
    private final File mp3File;

    private byte mpegVersion;
    private byte layer;
    private boolean protection;
    private long bitRate;
    private boolean variableBitRate;
    private double frequency;
    private boolean padding;
    private boolean privacy;
    private byte mode;
    private byte modeExtension;
    private boolean copyProtected;
    private boolean home;
    private byte emphasis;

    public MP3Info(String fileName)
    {
        this(new File(fileName));
    }

    public MP3Info(File mp3File)
    {
        this.mp3File = mp3File;
        try
        {
            seekMP3Frame();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file that
     * this object refers to. This is the first byte of music data and not the
     * ID3 Tag Frame.
     * 
     * @return true if the first MP3 frame can be found
     * 
     * @throws IOException
     *             on any I/O error
     */
    public boolean seekMP3Frame() throws IOException
    {
        RandomAccessFile rfile = null;
        boolean found = false;
        try
        {
            rfile = new RandomAccessFile(this.mp3File, "r");
            found = seekMP3Frame(rfile);
        } finally
        {
            if (rfile != null)
            {
                rfile.close();
            }
        }
        return found;
    }

    /**
     * Reads the mp3 frame header from the current position in the file and sets
     * this object's private variables to what is found. It assumes the
     * <code>RandomAccessFile</code> is already pointing to a valid MP3 Frame.
     * 
     * @param file
     *            File to read frame header
     * 
     * @throws IOException
     *             on any I/O error
     * @throws TagNotFoundException
     *             if MP3 Frame sync bites were not immediately found
     * @throws InvalidTagException
     *             if any of the header values are invalid
     */
    private void readFrameHeader(final RandomAccessFile file) throws IOException, MP3InfoException
    {
        final byte[] buffer = new byte[4];
        file.read(buffer);

        // sync
        if ((buffer[0] != (byte) 0xFF) || ((buffer[1] & (byte) 0xE0) != (byte) 0xE0))
        {
            throw new MP3InfoException("MP3 Frame sync bits not found");
        }
        this.mpegVersion = (byte) ((buffer[1] & MP3Constants.MASK_MP3_VERSION) >> 3);
        this.layer = (byte) ((buffer[1] & MP3Constants.MASK_MP3_LAYER) >> 1);
        this.protection = (buffer[1] & MP3Constants.MASK_MP3_PROTECTION) != 1;
        final int bitRateValue = (buffer[2] & MP3Constants.MASK_MP3_BITRATE) | (buffer[1] & MP3Constants.MASK_MP3_ID)
                | (buffer[1] & MP3Constants.MASK_MP3_LAYER);
        final Long object = (Long) MP3Constants.getBitrate(new Long(bitRateValue));
        if (object != null)
        {
            if (object.longValue() != this.bitRate)
            {
                this.variableBitRate = true;
            }
            this.bitRate = object.intValue();
        } else
        {
            throw new MP3InfoException("Invalid bit rate");
        }
        final int frequencyValue = (buffer[2] & MP3Constants.MASK_MP3_FREQUENCY) >>> 2;
        if (this.mpegVersion == 3)
        { // Version 1.0
            switch (frequencyValue)
            {
                case 0:
                    this.frequency = 44.1;
                    break;
                case 1:
                    this.frequency = 48.0;
                    break;
                case 2:
                    this.frequency = 32.0;
                    break;
            }
        } else if (this.mpegVersion == 2)
        { // Version 2.0
            switch (frequencyValue)
            {
                case 0:
                    this.frequency = 22.05;
                    break;
                case 1:
                    this.frequency = 24.00;
                    break;
                case 2:
                    this.frequency = 16.00;
                    break;
            }
        } else if (this.mpegVersion == 00)
        { // Version 2.5
            switch (frequencyValue)
            {
                case 0:
                    this.frequency = 11.025;
                    break;
                case 1:
                    this.frequency = 12.00;
                    break;
                case 2:
                    this.frequency = 8.00;
                    break;
            }
        } else
        {
            throw new MP3InfoException("Invalid MPEG version");
        }
        this.padding = (buffer[2] & MP3Constants.MASK_MP3_PADDING) != 0;
        this.privacy = (buffer[2] & MP3Constants.MASK_MP3_PRIVACY) != 0;
        this.mode = (byte) ((buffer[3] & MP3Constants.MASK_MP3_MODE) >> 6);
        this.modeExtension = (byte) ((buffer[3] & MP3Constants.MASK_MP3_MODE_EXTENSION) >> 4);
        this.copyProtected = (buffer[3] & MP3Constants.MASK_MP3_COPY) != 0;
        this.home = (buffer[3] & MP3Constants.MASK_MP3_HOME) != 0;
        this.emphasis = (byte) ((buffer[3] & MP3Constants.MASK_MP3_EMPHASIS));
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file
     * argument. It tries to sync as many frame as defined in
     * <code>TagOptions.getNumberMP3SyncFrame</code> This is the first byte of
     * music data and not the ID3 Tag Frame.
     * 
     * @param seekFile
     *            MP3 file to seek
     * 
     * @return true if the first MP3 frame can be found
     * 
     * @throws IOException
     *             on any I/O error
     */
    public boolean seekMP3Frame(final RandomAccessFile seekFile) throws IOException
    {
        boolean syncFound = false;
        byte first;
        byte second;
        long filePointer = 1;
        this.variableBitRate = false;
        try
        {
            seekFile.seek(0);
            do
            {
                first = seekFile.readByte();
                if (first == (byte) 0xFF)
                {
                    filePointer = seekFile.getFilePointer();
                    second = (byte) (seekFile.readByte() & (byte) 0xE0);
                    if (second == (byte) 0xE0)
                    {
                        seekFile.seek(filePointer - 1);

                        // seek the next frames, recursively
                        syncFound = seekNextMP3Frame(seekFile, 5);
                    }
                    seekFile.seek(filePointer);
                }
            } while (syncFound == false);
            seekFile.seek(filePointer - 1);
        } catch (EOFException ex)
        {
            syncFound = false;
        } catch (IOException ex)
        {
            throw ex;
        }
        return syncFound;
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file
     * argument. It is recursive and called by seekMP3Frame. This is the first
     * byte of music data and not the ID3 Tag Frame.
     * 
     * @param file
     *            MP3 file to seek
     * @param iterations
     *            recursive counter
     * 
     * @return true if the first MP3 frame can be found
     * 
     * @throws IOException
     *             on any I/O error
     */
    private boolean seekNextMP3Frame(final RandomAccessFile file, final int iterations) throws IOException
    {
        final boolean syncFound;
        final byte[] buffer;
        final byte first;
        final byte second;
        final long filePointer;
        if (iterations == 0)
        {
            syncFound = true;
        } else
        {
            try
            {
                readFrameHeader(file);
            } catch (MP3InfoException ex)
            {
                return false;
            }
            final int size = getFrameSize();
            if ((size <= 0) || (size > file.length()))
            {
                return false;
            }
            buffer = new byte[size - 4];
            file.read(buffer);
            filePointer = file.getFilePointer();
            first = file.readByte();
            if (first == (byte) 0xFF)
            {
                second = (byte) (file.readByte() & (byte) 0xE0);
                if (second == (byte) 0xE0)
                {
                    file.seek(filePointer);

                    // recursively find the next frames
                    syncFound = seekNextMP3Frame(file, iterations - 1);
                } else
                {
                    syncFound = false;
                }
            } else
            {
                syncFound = false;
            }
        }
        return syncFound;
    }

    /**
     * Returns the MP3 frame size for the file this object refers to. It assumes
     * that <code>seekNextMP3Frame</code> has already been called.
     * 
     * @return MP3 Frame size in bytes.
     */
    private int getFrameSize()
    {
        if (this.frequency == 0)
        {
            return 0;
        }
        final int size;
        final int paddingByte = this.padding ? 1 : 0;
        if (this.layer == 3)
        { // Layer I
            size = (int) ((((12 * this.bitRate) / this.frequency) + paddingByte) * 4);
        } else
        {
            size = (int) (((144 * this.bitRate) / this.frequency) + paddingByte);
        }
        return size;
    }
    
    public byte getMpegVersion()
    {
        return this.mpegVersion;
    }

    public byte getLayer()
    {
        return this.layer;
    }

    public boolean isProtection()
    {
        return this.protection;
    }

    public long getBitRate()
    {
        return this.bitRate;
    }

    public boolean isVariableBitRate()
    {
        return this.variableBitRate;
    }

    public double getFrequency()
    {
        return this.frequency;
    }

    public boolean isPadding()
    {
        return this.padding;
    }

    public boolean isPrivacy()
    {
        return this.privacy;
    }

    public byte getMode()
    {
        return this.mode;
    }

    public byte getModeExtension()
    {
        return this.modeExtension;
    }

    public boolean isCopyProtected()
    {
        return this.copyProtected;
    }

    public boolean isHome()
    {
        return this.home;
    }

    public byte getEmphasis()
    {
        return this.emphasis;
    }

}